package org.romov.vocab.common;

import java.util.*;
import org.w3c.dom.*;

/**
 * Implementation of vocabulary class  
 * @author Peter Romov <romovpa@gmail.com>
 */
public class Vocabulary {

	/**
	 * Main constructor, creates empty vocabulary
	 */
	public Vocabulary() {
		languages = new TreeMap<String, Language>();
		partsOfSpeech = new TreeMap<String, PartOfSpeech>();
		tags = new TreeMap<String, Tag>();
		units = new HashSet<Unit>();
		tagMap = new TreeMap<Tag, Vector<Unit>>();
	}
	
	/**
	 * Implementation of Unit class
	 * <p>Unit is a vocabulary entry, which have a part of speech 
	 * attribute and consist of a set of meanings. Unit object has a 
	 * set of pointers to tags, by which unit is marked.</p>
	 */
	public static class Unit  {
		
		/**
		 * Constructor, creates an empty unit
		 * @param part part of speech of the new unit
		 */
		public Unit(PartOfSpeech part) {
			this.part = part;
			meanings = new TreeSet<Meaning>();
			tags = new TreeSet<Tag>();
		}
		
		/**
		 * Returns unit part of speech
		 * @return part of speech object 
		 */
		public PartOfSpeech getPartOfSpeech() {
			return part;
		}
		
		/**
		 * Returns true if unit is marked by tag
		 * @param tag tag object
		 * @return is unit marked by tag
		 */
		public boolean isTagged(Tag tag) {
			return tags.contains(tag);
		}

		/**
		 * Returns set of meanings
		 * @return set of meanings
		 */
		public Set<Meaning> meanings() {
			return meanings;
		}
		
		/**
		 * Returns set of tags		
		 * @return set of tags
		 */
		public Set<Tag> tags() {
			return tags;
		}
		
		@Override
		public String toString() {
			return "{tags=" + tags + ", meanings=" + meanings + "}";
		}
		
		private PartOfSpeech part;
		private Set<Meaning> meanings;
		private Set<Tag> tags;
	}
	
	/**
	 * Implementation of Meaning class
	 * <p>Meaning is lexical item in vocabulary, it can be single 
	 * word in one of the languages or complex string expression.
	 * Meaning object has a pointer to unit object, which contains 
	 * this meaning.</p>
	 */
	public static class Meaning implements Comparable<Meaning> {
		
		/**
		 * Constructor, creates new meaning which don't append 
		 * to any unit
		 * @param lang language of new meaning
		 * @param content text value of new meaning
		 */
		public Meaning(Language lang, String content) {
			this.lang = lang;
			this.content = content;
			owner = null;
		}
		
		/**
		 * Returns meaning language
		 * @return language object
		 */
		public Language getLanguage() {
			return lang;
		}
		
		/**
		 * Returns meaning text content 
		 * @return content of meaning
		 */
		public String getContent() {
			return content;
		}
		
		/**
		 * Returns unit which contains this meaning
		 * @return unit object
		 */
		public Unit getUnit() {
			return owner;
		}
		
		@Override
		public int compareTo(Meaning other) {
			return content.compareTo(other.content);
		}
		
		@Override
		public String toString() {
			return "{lang=" + lang + ", content=" + content + "}";
		}
		
		private Language lang;
		private String content;
		private Unit owner;
	}

	/**
	 * Implementation of Language class
	 * <p>Language object is identifier of some language in the 
	 * vocabulary. It have several attributes and map, which reflects
	 * words to their meanings.</p>
	 */
	public static class Language implements Comparable<Language> {
		
		/**
		 * Constructor, creates new language with empty words map
		 * @param id short text identifier of language
		 * @param name full language name
		 */
		public Language(String id, String name) {
			this.id = id;
			this.name = name;
			wordsMap = new TreeMap<String, Meaning>();
		}
		
		/**
		 * Returns language identifier
		 * @return language id
		 */
		public String getId() {
			return id;
		}
		
		/**
		 * Returns language name
		 * @return language name
		 */
		public String getName() {
			return name;
		}
		
		/**
		 * Returns meaning according to language and content
		 * @param content content of meaning
		 * @return meaning object
		 */
		public Meaning getMeaning(String content) {
			return wordsMap.get(content);
		}
		
		/**
		 * Returns collection of meanings with this language
		 * @return collection of meanings
		 */
		public Collection<Meaning> meanings() {
			return wordsMap.values();
		}
		
		@Override
		public int compareTo(Language other) {
			return id.compareTo(other.id);
		}
		
		@Override
		public String toString() {
			return id + "(" + name + ")";
		}
		
		private String id;
		private String name;
		private Map<String, Meaning> wordsMap;
	}
	
	/**
	 * Implementation of PartOfSpeech class
	 * <p>PartOfSpeech object is indentifier of some part of speech
	 * in the vocabulary. It have a set of pointers to units, which 
	 * consider to this part of speech.</p> 
	 */
	public static class PartOfSpeech implements Comparable<PartOfSpeech> {
		
		/**
		 * Constructor, creates new part of speech with empty units set
		 * @param id text identifier of the new part of speech
		 */
		public PartOfSpeech(String id) {
			this.id = id;
			units = new TreeSet<Unit>();
		}
		
		/**
		 * Returns part of speech identifier
		 * @return part of speech id
		 */
		public String getId() {
			return id;
		}
		
		/** 
		 * Returns set of units with this part of speech
		 * @return set of units
		 */
		public Set<Unit> units() {
			return units;
		}
		
		@Override
		public int compareTo(PartOfSpeech other) {
			return id.compareTo(other.id);
		}
		
		@Override
		public String toString() {
			return id;
		}
		
		private String id;
		private Set<Unit> units;
	}
	
	/**
	 * Implementation of Tag class
	 * <p>Tag is some text mark, which can be assigned to unit. Tag 
	 * object has a set of pointers to units, marked with this tag.</p>
	 */
	public static class Tag implements Comparable<Tag> {
		
		/**
		 * Constructor, creates new tag with empty units set
		 * @param name text name of the new tag 
		 */
		public Tag(String name) {
			this.name = name;
			units = new HashSet<Unit>();
		}
		
		/**
		 * Returns tag name
		 * @return tag name
		 */
		public String getName() {
			return name;
		}
		
		/**
		 * Returns set of units tagged by this tag
		 * @return set of units
		 */
		public Set<Unit> units() {
			return units;
		}
		
		@Override
		public int compareTo(Tag other) {
			return name.compareTo(other.name);
		}
		
		@Override
		public String toString() {
			return name;
		}
		
		private String name;
		private Set<Unit> units;
	}
	
	/**
	 * Parse vocabulary from XML document
	 * @param doc input XML document
	 * @return parsed vocabulary
	 */
	public static Vocabulary fromDocument(Document doc) {
		// TODO: Semantic errors handling
		Vocabulary res = new Vocabulary();
		// Parse <vocabulary> element
		Element root = doc.getDocumentElement();
		if (root.getTagName().equals("vocabulary")) {
			// Parse <languages> element
			try {
				Element languagesElem = (Element)root
					.getElementsByTagName("languages").item(0);
				NodeList languagesChilds = languagesElem.getElementsByTagName("lang");
				for (int i = 0; i < languagesChilds.getLength(); i++) {
					Element langElem = (Element)languagesChilds.item(i);
					// Parse <lang> element
					String langId = langElem.getAttribute("id");
					if (res.languages.containsKey(langId))
						// Format Error: duplicate of language id
						return null;
					Language langObj = new Language(langId, langElem.getTextContent());
					res.languages.put(langId, langObj);					
				}
			} 
			catch (ArrayIndexOutOfBoundsException e) {
				// Format Error: tag <languages> is absent 
				return null;
			}
			// Parse <partsOfSpeech> elements
			try {
				Element partsOfSpeechElem = (Element)root
					.getElementsByTagName("partsOfSpeech").item(0);
				NodeList partsOfSpeechChilds = partsOfSpeechElem.getElementsByTagName("part");
				for (int i = 0; i < partsOfSpeechChilds.getLength(); i++) {
					Element langElem = (Element)partsOfSpeechChilds.item(i);
					// Parse <part> element
					String partId = langElem.getAttribute("id");
					if (res.partsOfSpeech.containsKey(partId))
						// Format Error: duplicate of part id
						return null;
					PartOfSpeech partObj = new PartOfSpeech(partId);
					res.partsOfSpeech.put(partObj.id, partObj);
				}
			} 
			catch (ArrayIndexOutOfBoundsException e) {
				// Format Error: tag <partsOfSpeech> is absent
				return null;
			}
			// Parse <units> elements
			try {
				Element unitsElem = (Element)root.getElementsByTagName("units").item(0);
				NodeList unitsChilds = unitsElem.getElementsByTagName("unit");
				for (int i = 0; i < unitsChilds.getLength(); i++) {
					Element unitElem = (Element)unitsChilds.item(i);
					// Parse <unit> element
					PartOfSpeech partObj = res.getPartOfSpeech(unitElem.getAttribute("part"));
					if (partObj == null)
						// Format Error: bad part id
						return null;
					Unit unitObj = new Unit(partObj);
					NodeList unitMeaningChilds = unitElem.getElementsByTagName("meaning");
					for (int j = 0; j < unitMeaningChilds.getLength(); j++) {
						Element meaningElem = (Element)unitMeaningChilds.item(j);
						// Parse <meaning> element
						Language langObj = res.getLanguage(meaningElem.getAttribute("lang"));
						if (langObj == null)
							// Format Error: bad language id
							return null;
						Meaning meaningObj = new Meaning(langObj, meaningElem.getTextContent());
						meaningObj.owner = unitObj;
						langObj.wordsMap.put(meaningObj.content, meaningObj);
						unitObj.meanings.add(meaningObj);
					}
					NodeList unitTagChilds = unitElem.getElementsByTagName("tag");
					for (int j = 0; j < unitTagChilds.getLength(); j++) {
						Element tagElem = (Element)unitTagChilds.item(j);
						// Parse <tag> element
						Tag tagObj = res.getTag(tagElem.getTextContent());
						unitObj.tags.add(tagObj);
						tagObj.units.add(unitObj);
					}
					res.units.add(unitObj);
				}
			}
			catch (ArrayIndexOutOfBoundsException e) { 
				// Format Error: tag <units> is absent
				return null;
			}
		} 
		else 
			return null;
		return res;
	}
	
	/**
	 * Writes vocabulary to empty XML document
	 * @param doc empty document
	 */
	public void makeDocument(Document doc) {
		Element root = doc.createElement("vocabulary");
		// Generating <languages> element
		Element languagesElement = doc.createElement("languages");
		for (Language lang : languages.values()) {
			Element langElement = doc.createElement("lang");
			langElement.setAttribute("id", lang.id);
			langElement.setTextContent(lang.name);
			languagesElement.appendChild(langElement);
		}
		root.appendChild(languagesElement);
		// Generating <partsOfSpeech> element
		Element partsOfSpeechElement = doc.createElement("partsOfSpeech");
		for (PartOfSpeech part : partsOfSpeech.values()) {
			Element partElement = doc.createElement("part");
			partElement.setAttribute("id", part.id);
			partsOfSpeechElement.appendChild(partElement);
		}
		root.appendChild(partsOfSpeechElement);
		// Generating <units> element
		Element unitsElement = doc.createElement("units");
		for (Unit unit : units) {
			Element unitElement = doc.createElement("unit");
			unitElement.setAttribute("part", unit.part.id);
			// Generate <meaning> elements
			for (Meaning meaning : unit.meanings) {
				Element meaningElement = doc.createElement("meaning");
				meaningElement.setAttribute("lang", meaning.lang.id);
				meaningElement.setTextContent(meaning.content);
				unitElement.appendChild(meaningElement);
			}
			// Generate <tag> elements
			for (Tag tag : unit.tags) {
				Element tagElement = doc.createElement("tag");
				tagElement.setTextContent(tag.name);
				unitElement.appendChild(tagElement);
			}
			unitsElement.appendChild(unitElement);
		}
		root.appendChild(unitsElement);
		// Appending root
		doc.appendChild(root);
	}
	
	/**
	 * Returns tag object according to tag name, creates new tag 
	 * object if it is not exist.
	 * @param name tag name
	 * @return Tag object
	 */
	public Tag getTag(String name) {
		Tag tag = tags.get(name);
		if (tag == null) {
			tag = new Tag(name);
			tags.put(name, tag);
		}
		return tag;
	}
	
	/**
	 * Returns language object according to language id. 
	 * @param id language identifier
	 * @return language object
	 */
	public Language getLanguage(String id) {
		return languages.get(id);
	}
	
	/**
	 * Returns part of speech object according to its id.
	 * @param id part of speech identifier
	 * @return part of speech object
	 */
	public PartOfSpeech getPartOfSpeech(String id) {
		return partsOfSpeech.get(id);
	}
	
	private Map<String, Language> languages;
	private Map<String, PartOfSpeech> partsOfSpeech;
	private Map<String, Tag> tags;
	private Set<Unit> units;
	private Map<Tag, Vector<Unit>> tagMap;	
}
